import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime as dt
import numpy as np
import datetime, os, wget, requests, unidecode, re
import uuid
import xlwings as xw
from lat_lon_parser import parse
from bs4 import BeautifulSoup
start_date = dt(2020, 11, 8)
end_date = dt(2021, 1, 26)
hours = ['14', '17', '21', '04', '08', '11']
last_day_hours = ['04', '08', '11', '14']
def download_excel(date, hour):
str_date = date.strftime("%Y%m%d")
url = f'https://www.vendeeglobe.org/download-race-data/vendeeglobe_{str_date}_{hour}0000.xlsx'
try:
if not os.path.exists(f'vg_data/vendeeglobe_{str_date}_{hour}0000.xlsx'):
wget.download(url, f'vg_data/vendeeglobe_{str_date}_{hour}0000.xlsx')
except Exception as e:
print(f'Error for {date.strftime("%Y-%m-%d")} at {hour}H:')
print(e)
date = start_date
while date <= end_date:
str_date = date.strftime("%Y%m%d")
for hour in hours:
download_excel(date, hour)
date += datetime.timedelta(days=1)
for hour in last_day_hours:
download_excel(date, hour)
if os.path.exists(f'vg_data/vendeeglobe_20201108_140000.xlsx'):
with xw.Book(f"vg_data/vendeeglobe_20201108_140000.xlsx", mode="r") as book:
sheet1 = book.sheets[0]
display(sheet1.cells.options("df").value.head(10))
Les données sont très sales. Il faut bien définir les noms de colonnes, éliminer les \r\n, parser les nombres sans leur mesures
def clean_excel(date, hour):
str_date = date.strftime("%Y%m%d")
data = None
if os.path.exists(f'vg_data/vendeeglobe_{str_date}_{hour}0000.xlsx'):
with xw.Book(f"vg_data/vendeeglobe_{str_date}_{hour}0000.xlsx", mode="r") as book:
sheet1 = book.sheets[0]
data = sheet1.cells.options("df").value
# création des noms de colonnes
data = data.iloc[2:]
data.iloc[1,:3] = data.iloc[0,:3]
data.iloc[1,-2:] = data.iloc[0,-2:]
data.iloc[1] = data.iloc[1].str.split("\r\n").str[0]
data.iloc[1, 1] = data.iloc[1, 1].replace(" / ",' ')
data.iloc[1, 2] = data.iloc[1, 2].replace(" / ",'\r\n')
data.iloc[1,6:10] = data.iloc[1,6:10]+"_30m"
data.iloc[1,10:14] = data.iloc[1,10:14]+"_last"
data.iloc[1,14:18] = data.iloc[1,14:18]+"_24h"
data.columns = data.iloc[1]
data = data.iloc[2:]
data = data.iloc[:-4]
# Séparation des colonnes Nationalités et Voile
nat_voile = "(?P<Nationalite>.*) (?P<Voile>[0-9]*)"
data[["Nationalité", "Voile"]] = data['Nat. Voile'].str.extract(nat_voile, expand=True)
# Séparation des colonnes Skipper et nom de bateau
skipper_bateau = "(?P<Skipper>.*)\r\n(?P<nBateau>.*)"
data[["Skipper", "Bateau"]] = data['Skipper\r\nBateau'].str.extract(skipper_bateau, expand=True)
data = data.drop(['Nat. Voile', 'Skipper\r\nBateau'], axis=1)
# Ajout de la date avec l'heure
data["date"] = date + datetime.timedelta(hours=int(hour))
data = data.reset_index(drop=True)
return data
# Lambda pour extraire les nombres des strings (tous les chiffres sont accompagnés de leurs mesures)
get_num = lambda x: re.findall(r'-?\d+\.?\d*', x)[0] if len(re.findall(r'-?\d+\.?\d*', x)) > 0 else np.nan
# Creation du dataframe comportant toute les données de la course
date = start_date
race_df = pd.DataFrame()
while date <= end_date + datetime.timedelta(days=1):
for hour in hours:
tmp_df = clean_excel(date, hour)
try :
race_df = pd.concat([race_df, tmp_df])
except Exception as e:
print(e)
break
date += datetime.timedelta(days=1)
# Certains compétiteur ont abandonné la course, on supprime alors les lignes après leurs abandon (beaucoup de nan sur ces lignes)
race_df = race_df.dropna(subset=race_df.columns[1:18]).reset_index(drop=True)
# Standardisation des nom
race_df["Skipper"] = race_df["Skipper"].map(lambda x: unidecode.unidecode(x.lower()))
# Conversion de la latitude et longitude en format decimal
race_df['Latitude'] = race_df['Latitude'].apply(parse)
race_df['Longitude'] = race_df['Longitude'].apply(parse)
race_df['Rang'] = race_df['Rang'].astype(int)
# Extraction des nombres sans leur unité de mesure
race_df.iloc[:,4:-5] = race_df.iloc[:,4:-5].applymap(get_num).astype(float)
race_df = race_df.drop("Heure FR", axis=1)
race_df.head()
race_df.describe()
race_df.info()
url = "https://www.vendeeglobe.org/fr/glossaire"
page = requests.get(url).content
soup = BeautifulSoup(page)
specs_ul = soup.find_all('ul', {"class","boats-list__popup-specs-list"})
dict_list = []
for spec_ul in specs_ul:
specs_list = [elem.string.split(' : ') for elem in spec_ul.find_all('li')]
specs_dict = {elem[0]: elem[1] for elem in specs_list}
# Le nom est disponibe dans un lien parents de la liste menant vers la fiche skipper. Ce lien contient son nom a la fin
skipper = spec_ul.parent()[0].parent()[-5]['href'].split('/')[-1].replace('-', ' ')
specs_dict["Skipper"] = skipper
dict_list.append(specs_dict)
specs_df = pd.DataFrame(dict_list).drop(16).set_index("Skipper")
specs_df.head()
Il était prévu à l'origine de faire le lien entre les specs et les bateaux par leur numero de voile. Malheureusement, ces derniers ne correspondent pas toujours. Il a donc été choisis de scraper le nom du skipper associé au bateau pour faire le lien avec les données de courses.
# Extraction des nombres sans leurs mesures
numerical_specs = specs_df.iloc[:, [5,6,7,8,10,12,13]].stack().str.replace(',','.').apply(get_num).unstack()
numerical_specs
NaN values, on utilise un knn pour inferer leur valeur.
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=2)
numerical_specs = pd.DataFrame(imputer.fit_transform(numerical_specs), columns=numerical_specs.columns, index=numerical_specs.index, dtype=float)
numerical_specs
clean_specs_df = specs_df.copy()
clean_specs_df.iloc[:, [5,6,7,8,10,12,13]] = numerical_specs
# Le nombre de dérives est soit 2 soit foil. On choisit donc de la rendre booleen en fonction de la présence ou nom d'un foil
clean_specs_df['Nombre de dérives'] = clean_specs_df['Nombre de dérives'].str.contains('foil')
clean_specs_df.rename(columns={"Nombre de dérives": "Foil"}, inplace=True)
clean_specs_df = clean_specs_df.drop(clean_specs_df.columns[[0,1]], axis = 1)
clean_specs_df.head()
Dans cette partie, nous allons visualiser, sur une carte, le trajet des k premiers compétiteurs. Cette carte est intéractive et permet de choisir l'avancée des compétiteurs en fonction de la date.
from ipywidgets import widgets
from ipyleaflet import Map, AntPath, WidgetControl, LegendControl
import seaborn as sns
def get_skipper_loc(race, name, until=None):
locations = race.loc[race['Skipper'] == name]
if until == None:
return locations[['Latitude', 'Longitude']].values.tolist()
return locations.loc[locations['date'] <= until, ['Latitude', 'Longitude']].values.tolist()
def get_top_k_skipper_race(race, k):
top_k_skippers = race.sort_values(by=['date', 'Rang'], ascending=[False, True]).head(k)['Skipper'].unique()
ordered_top_k_race = race.loc[race['Skipper'].isin(top_k_skippers)].sort_values(by=['Skipper', 'date'])
return top_k_skippers, ordered_top_k_race
nb_skipper = 6
main_palette = sns.color_palette("hls", nb_skipper, desat=0.7).as_hex()
dot_palette = sns.color_palette("hls", nb_skipper).as_hex()
top_k_skippers, ordered_top_k_race = get_top_k_skipper_race(race_df, nb_skipper)
ant_paths = {skipper : AntPath(
locations=get_skipper_loc(ordered_top_k_race, skipper),
dash_array=[1, 10],
delay=1000,
color=color,
pulse_color=pulse_color
) for skipper, color, pulse_color in zip(top_k_skippers[:nb_skipper], main_palette[:nb_skipper], dot_palette[:nb_skipper])}
def on_value_change(change):
new_date = change['new']
for skipper, ant_path in ant_paths.items():
ant_path.locations = get_skipper_loc(ordered_top_k_race, skipper, new_date)
days = [dt.fromordinal(i) for i in range(start_date.toordinal(), (end_date + datetime.timedelta(days=2)).toordinal())]
date_slider = widgets.SelectionSlider(
options=days,
description='Date',
disabled=False,
continuous_update=True,
orientation='horizontal',
readout=True
)
date_slider.observe(on_value_change, names='value')
m = Map(center=(-10.407666666666664, -1.8413333333333333), zoom=2)
for ant_path in ant_paths.values():
m.add_layer(ant_path)
widget_control1 = WidgetControl(widget=date_slider, position='topleft')
m.add_control(widget_control1)
legend = LegendControl({skipper: color for skipper, color in zip(top_k_skippers, main_palette)}, name="Skipper", position="topright")
m.add_control(legend)
display(m)
# Décommenter et exécuter pour fermer la carte intéractive ci dessus
# m.close()
top_10_skipper, top_10_race = get_top_k_skipper_race(race_df, 10)
grouped_race = top_10_race.pivot_table(values='Rang', index='date', columns='Skipper')
grouped_race.plot(figsize=(20, 10), ylabel='rang', title="Evolution du classement des 10 premiers")
plt.show()
Le classement bouge beaucoup les premiers jours mais se stabilise assez rapidement. On remarque qu'il y a eu tout de même une bagarre assez constante entre la 5ème et la 10ème places. Les skippers devaient être assez rapproché à ce niveau de la course.
import plotly.express as px
full_race = pd.merge(race_df, clean_specs_df, on="Skipper")
distance_table = full_race.pivot_table(values="Distance_last", index="date", columns = "Skipper").iloc[3:].cumsum()
fig = px.line(distance_table, title="Distance parcourue par compétiteur", labels={"value": "Distance parcourue (nm)"}, height=800)
fig.show()
On observe bien l'écart de distance parcourue. Le premier de la course avait 1 mois d'avances par rapport au dernier.
k = 25
full_top_k_race = full_race.sort_values(by=['date', 'Rang'], ascending=[False, True]).head(k)
mean_rank = full_top_k_race.groupby("Foil")['Rang'].mean()
mean_rank.plot.bar(ylabel='Rang', xlabel="Présence d'un foil", title='Rang moyen avec et sans Foil')
plt.show()
On remarque clairement l'impact du foil dans la cours. Un compétiteur avec foil fini en moyenne 5 places devant un compétiteur sans foil
sns.displot(full_race[["Rang", "Foil"]], x="Rang", hue="Foil", kind='kde')
plt.title("Densité du classement avec ou sans foil durant la compétition")
plt.show()
Autre visualisation permettant d'illustrer l'impact du foil dans la compétition. On voit bien le pic de densité sans foil entre la 25 - 30ème place tandis que la densité avec foil domine les 10 premieres places du classement.
sns.displot(full_race[["VMG_last", "Foil"]], x="VMG_last", hue="Foil", kind='kde')
plt.title("Densité de la vitesse utile avec ou sans foil durant la compétition")
plt.show()
On observe aussi que le foil impact positivement la vitesse utile des compétiteurs
moy_vmg = race_df.groupby("Skipper")["VMG_last"].mean().sort_values(ascending=False)
numerical_specs["VMG_moy"] = moy_vmg
corr_matrix = numerical_specs.drop(columns=["Longueur", "Tirant d'eau"]).corr()
corr_matrix.style.background_gradient(cmap='coolwarm')
Après une petite étude de la correlation des specs des bateaux par rapport à la vitesse moyenne, il semble que la caracteristique la plus impactante est la surface de voiles au près. Les autres caractéristiques semblent très peu corrélées.